{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
   "metadata": {},
   "source": [
    "# How to manage conversation history in a ReAct Agent\n",
    "\n",
    "!!! info \"Prerequisites\"\n",
    "    This guide assumes familiarity with the following:\n",
    "\n",
    "    - [Prebuilt create_react_agent](../create-react-agent)\n",
    "    - [Persistence](../../concepts/persistence)\n",
    "    - [Short-term Memory](../../concepts/memory/#short-term-memory)\n",
    "    - [Trimming Messages](https://python.langchain.com/docs/how_to/trim_messages/)\n",
    "\n",
    "Message history can grow quickly and exceed LLM context window size, whether you're building chatbots with many conversation turns or agentic systems with numerous tool calls. There are several strategies for managing the message history:\n",
    "\n",
    "* [message trimming](#keep-the-original-message-history-unmodified) — remove first or last N messages in the history\n",
    "* [summarization](#summarizing-message-history) — summarize earlier messages in the history and replace them with a summary\n",
    "* custom strategies (e.g., message filtering, etc.)\n",
    "\n",
    "To manage message history in `create_react_agent`, you need to define a `pre_model_hook` function or [runnable](https://python.langchain.com/docs/concepts/runnables/) that takes graph state an returns a state update:\n",
    "\n",
    "\n",
    "* Trimming example:\n",
    "    ```python\n",
    "    # highlight-next-line\n",
    "    from langchain_core.messages.utils import (\n",
    "        # highlight-next-line\n",
    "        trim_messages, \n",
    "        # highlight-next-line\n",
    "        count_tokens_approximately\n",
    "    # highlight-next-line\n",
    "    )\n",
    "    from langgraph.prebuilt import create_react_agent\n",
    "    \n",
    "    # This function will be called every time before the node that calls LLM\n",
    "    def pre_model_hook(state):\n",
    "        trimmed_messages = trim_messages(\n",
    "            state[\"messages\"],\n",
    "            strategy=\"last\",\n",
    "            token_counter=count_tokens_approximately,\n",
    "            max_tokens=384,\n",
    "            start_on=\"human\",\n",
    "            end_on=(\"human\", \"tool\"),\n",
    "        )\n",
    "        # You can return updated messages either under `llm_input_messages` or \n",
    "        # `messages` key (see the note below)\n",
    "        # highlight-next-line\n",
    "        return {\"llm_input_messages\": trimmed_messages}\n",
    "\n",
    "    checkpointer = InMemorySaver()\n",
    "    agent = create_react_agent(\n",
    "        model,\n",
    "        tools,\n",
    "        # highlight-next-line\n",
    "        pre_model_hook=pre_model_hook,\n",
    "        checkpointer=checkpointer,\n",
    "    )\n",
    "    ```\n",
    "\n",
    "* Summarization example:\n",
    "    ```python\n",
    "    # highlight-next-line\n",
    "    from langmem.short_term import SummarizationNode\n",
    "    from langchain_core.messages.utils import count_tokens_approximately\n",
    "    from langgraph.prebuilt.chat_agent_executor import AgentState\n",
    "    from langgraph.checkpoint.memory import InMemorySaver\n",
    "    from typing import Any\n",
    "    \n",
    "    model = ChatOpenAI(model=\"gpt-4o\")\n",
    "    \n",
    "    summarization_node = SummarizationNode(\n",
    "        token_counter=count_tokens_approximately,\n",
    "        model=model,\n",
    "        max_tokens=384,\n",
    "        max_summary_tokens=128,\n",
    "        output_messages_key=\"llm_input_messages\",\n",
    "    )\n",
    "\n",
    "    class State(AgentState):\n",
    "        # NOTE: we're adding this key to keep track of previous summary information\n",
    "        # to make sure we're not summarizing on every LLM call\n",
    "        # highlight-next-line\n",
    "        context: dict[str, Any]\n",
    "    \n",
    "    \n",
    "    checkpointer = InMemorySaver()\n",
    "    graph = create_react_agent(\n",
    "        model,\n",
    "        tools,\n",
    "        # highlight-next-line\n",
    "        pre_model_hook=summarization_node,\n",
    "        # highlight-next-line\n",
    "        state_schema=State,\n",
    "        checkpointer=checkpointer,\n",
    "    )\n",
    "    ```\n",
    "\n",
    "!!! Important\n",
    "    \n",
    "    * To **keep the original message history unmodified** in the graph state and pass the updated history **only as the input to the LLM**, return updated messages under `llm_input_messages` key\n",
    "    * To **overwrite the original message history** in the graph state with the updated history, return updated messages under `messages` key\n",
    "    \n",
    "    To overwrite the `messages` key, you need to do the following:\n",
    "\n",
    "    ```python\n",
    "    from langchain_core.messages import RemoveMessage\n",
    "    from langgraph.graph.message import REMOVE_ALL_MESSAGES\n",
    "\n",
    "    def pre_model_hook(state):\n",
    "        updated_messages = ...\n",
    "        return {\n",
    "            \"messages\": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *updated_messages]\n",
    "            ...\n",
    "        }\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
   "metadata": {},
   "source": [
    "## Setup\n",
    "\n",
    "First, let's install the required packages and set our API keys"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "a213e11a-5c62-4ddb-a707-490d91add383",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install -U langgraph langchain-openai langmem"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23a1885c-04ab-4750-aefa-105891fddf3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "\n",
    "def _set_env(var: str):\n",
    "    if not os.environ.get(var):\n",
    "        os.environ[var] = getpass.getpass(f\"{var}: \")\n",
    "\n",
    "\n",
    "_set_env(\"OPENAI_API_KEY\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87a00ce9",
   "metadata": {},
   "source": [
    "<div class=\"admonition tip\">\n",
    "    <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
    "    <p style=\"padding-top: 5px;\">\n",
    "        Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
    "    </p>\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
   "metadata": {},
   "source": [
    "## Keep the original message history unmodified"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd6cbd3a-8632-47ae-9ec5-eec8d7b05cae",
   "metadata": {},
   "source": [
    "Let's build a ReAct agent with a step that manages the conversation history: when the length of the history exceeds a specified number of tokens, we will call [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) utility that that will reduce the history while satisfying LLM provider constraints.\n",
    "\n",
    "There are two ways that the updated message history can be applied inside ReAct agent:\n",
    "\n",
    "  * [**Keep the original message history unmodified**](#keep-the-original-message-history-unmodified) in the graph state and pass the updated history **only as the input to the LLM**\n",
    "  * [**Overwrite the original message history**](#overwrite-the-original-message-history) in the graph state with the updated history\n",
    "\n",
    "Let's start by implementing the first one. We'll need to first define model and tools for our agent:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "eaad19ee-e174-4c6c-b2b8-3530d7acea40",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
    "\n",
    "\n",
    "def get_weather(location: str) -> str:\n",
    "    \"\"\"Use this to get weather information.\"\"\"\n",
    "    if any([city in location.lower() for city in [\"nyc\", \"new york city\"]]):\n",
    "        return \"It might be cloudy in nyc, with a chance of rain and temperatures up to 80 degrees.\"\n",
    "    elif any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
    "        return \"It's always sunny in sf\"\n",
    "    else:\n",
    "        return f\"I am not sure what the weather is in {location}\"\n",
    "\n",
    "\n",
    "tools = [get_weather]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52402333-61ab-47d3-8549-6a70f6f1cf36",
   "metadata": {},
   "source": [
    "Now let's implement `pre_model_hook` — a function that will be added as a new node and called every time **before** the node that calls the LLM (the `agent` node).\n",
    "\n",
    "Our implementation will wrap the `trim_messages` call and return the trimmed messages under `llm_input_messages`. This will **keep the original message history unmodified** in the graph state and pass the updated history **only as the input to the LLM**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "b507eb58-6e02-4ac6-b48b-ea4defdc11f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.prebuilt import create_react_agent\n",
    "from langgraph.checkpoint.memory import InMemorySaver\n",
    "\n",
    "# highlight-next-line\n",
    "from langchain_core.messages.utils import (\n",
    "    # highlight-next-line\n",
    "    trim_messages,\n",
    "    # highlight-next-line\n",
    "    count_tokens_approximately,\n",
    "    # highlight-next-line\n",
    ")\n",
    "\n",
    "\n",
    "# This function will be added as a new node in ReAct agent graph\n",
    "# that will run every time before the node that calls the LLM.\n",
    "# The messages returned by this function will be the input to the LLM.\n",
    "def pre_model_hook(state):\n",
    "    trimmed_messages = trim_messages(\n",
    "        state[\"messages\"],\n",
    "        strategy=\"last\",\n",
    "        token_counter=count_tokens_approximately,\n",
    "        max_tokens=384,\n",
    "        start_on=\"human\",\n",
    "        end_on=(\"human\", \"tool\"),\n",
    "    )\n",
    "    # highlight-next-line\n",
    "    return {\"llm_input_messages\": trimmed_messages}\n",
    "\n",
    "\n",
    "checkpointer = InMemorySaver()\n",
    "graph = create_react_agent(\n",
    "    model,\n",
    "    tools,\n",
    "    # highlight-next-line\n",
    "    pre_model_hook=pre_model_hook,\n",
    "    checkpointer=checkpointer,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "8182ab45-86b3-4d6f-b75e-58862a14fa4e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import display, Image\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d41e8e76-5d43-44cd-bf01-a39212cedd8d",
   "metadata": {},
   "source": [
    "We'll also define a utility to render the agent outputs nicely:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_stream(stream, output_messages_key=\"llm_input_messages\"):\n",
    "    for chunk in stream:\n",
    "        for node, update in chunk.items():\n",
    "            print(f\"Update from node: {node}\")\n",
    "            messages_key = (\n",
    "                output_messages_key if node == \"pre_model_hook\" else \"messages\"\n",
    "            )\n",
    "            for message in update[messages_key]:\n",
    "                if isinstance(message, tuple):\n",
    "                    print(message)\n",
    "                else:\n",
    "                    message.pretty_print()\n",
    "\n",
    "        print(\"\\n\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84448d29-b323-4833-80fc-4fff2f5a0950",
   "metadata": {},
   "source": [
    "Now let's run the agent with a few different queries to reach the specified max tokens limit:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
    "result = graph.invoke(inputs, config=config)\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
    "result = graph.invoke(inputs, config=config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fdb186da-b55d-4cb8-a237-e9e157ab0458",
   "metadata": {},
   "source": [
    "Let's see how many tokens we have in the message history so far:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "41ba0253-5199-4d29-82ae-258cbbebddb4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "415"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "messages = result[\"messages\"]\n",
    "count_tokens_approximately(messages)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "812987ac-66ba-4122-8281-469cbdced7c7",
   "metadata": {},
   "source": [
    "You can see that we are close to the `max_tokens` threshold, so on the next invocation we should see `pre_model_hook` kick-in and trim the message history. Let's run it again:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "26c53429-90ba-4d0b-abb9-423d9120ad26",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: pre_model_hook\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What's it known for?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "New York City is known for a variety of iconic landmarks, cultural institutions, and vibrant neighborhoods. Some of the most notable features include:\n",
      "\n",
      "1. **Statue of Liberty**: A symbol of freedom and democracy, located on Liberty Island.\n",
      "2. **Times Square**: Known for its bright lights, Broadway theaters, and bustling atmosphere.\n",
      "3. **Central Park**: A large public park offering a natural retreat in the middle of the city.\n",
      "4. **Empire State Building**: An iconic skyscraper offering panoramic views of the city.\n",
      "5. **Broadway**: Famous for its world-class theater productions.\n",
      "6. **Wall Street**: The financial hub of the United States.\n",
      "7. **Museums**: Including the Metropolitan Museum of Art, Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
      "8. **Diverse Cuisine**: A melting pot of cultures offering a wide range of culinary experiences.\n",
      "9. **Cultural Diversity**: A rich tapestry of cultures and communities from around the world.\n",
      "10. **Fashion**: A global fashion capital, hosting events like New York Fashion Week.\n",
      "\n",
      "These are just a few highlights of what makes New York City a unique and vibrant place.\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "where can i find the best bagel?\n",
      "\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "New York City is famous for its bagels, and there are several places renowned for serving some of the best. Here are a few top spots where you can find excellent bagels in NYC:\n",
      "\n",
      "1. **Ess-a-Bagel**: Known for their large, chewy bagels with a variety of spreads and toppings.\n",
      "2. **Russ & Daughters**: A classic spot offering traditional bagels with high-quality smoked fish and cream cheese.\n",
      "3. **H&H Bagels**: Famous for their fresh, hand-rolled bagels.\n",
      "4. **Murray’s Bagels**: Offers a wide selection of bagels and spreads, with a no-toasting policy to preserve freshness.\n",
      "5. **Absolute Bagels**: Known for their authentic, fluffy bagels and a variety of cream cheese options.\n",
      "6. **Tompkins Square Bagels**: Offers creative bagel sandwiches and a wide range of spreads.\n",
      "7. **Bagel Hole**: Known for their smaller, denser bagels with a crispy crust.\n",
      "\n",
      "Each of these places has its own unique style and flavor, so it might be worth trying a few to find your personal favorite!\n",
      "\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
    "print_stream(graph.stream(inputs, config=config, stream_mode=\"updates\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58fe0399-4e7d-4482-a4cb-5301311932d0",
   "metadata": {},
   "source": [
    "You can see that the `pre_model_hook` node now only returned the last 3 messages, as expected. However, the existing message history is untouched:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "7ecfc310-8f9e-4aa0-9e58-17e71551639a",
   "metadata": {},
   "outputs": [],
   "source": [
    "updated_messages = graph.get_state(config).values[\"messages\"]\n",
    "assert [(m.type, m.content) for m in updated_messages[: len(messages)]] == [\n",
    "    (m.type, m.content) for m in messages\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "035864e3-0083-4dea-bf85-3a702fa5303f",
   "metadata": {},
   "source": [
    "## Overwrite the original message history"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b0a4fd5-a2ba-4eca-91a9-d294f4f2d884",
   "metadata": {},
   "source": [
    "Let's now change the `pre_model_hook` to **overwrite** the message history in the graph state. To do this, we’ll return the updated messages under `messages` key. We’ll also include a special `RemoveMessage(REMOVE_ALL_MESSAGES)` object, which tells `create_react_agent` to remove previous messages from the graph state:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "48c2a65b-685a-4750-baa6-2d61efe76b5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import RemoveMessage\n",
    "from langgraph.graph.message import REMOVE_ALL_MESSAGES\n",
    "\n",
    "\n",
    "def pre_model_hook(state):\n",
    "    trimmed_messages = trim_messages(\n",
    "        state[\"messages\"],\n",
    "        strategy=\"last\",\n",
    "        token_counter=count_tokens_approximately,\n",
    "        max_tokens=384,\n",
    "        start_on=\"human\",\n",
    "        end_on=(\"human\", \"tool\"),\n",
    "    )\n",
    "    # NOTE that we're now returning the messages under the `messages` key\n",
    "    # We also remove the existing messages in the history to ensure we're overwriting the history\n",
    "    # highlight-next-line\n",
    "    return {\"messages\": [RemoveMessage(REMOVE_ALL_MESSAGES)] + trimmed_messages}\n",
    "\n",
    "\n",
    "checkpointer = InMemorySaver()\n",
    "graph = create_react_agent(\n",
    "    model,\n",
    "    tools,\n",
    "    # highlight-next-line\n",
    "    pre_model_hook=pre_model_hook,\n",
    "    checkpointer=checkpointer,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd061682-231c-4487-9c2f-a6820dfbcab7",
   "metadata": {},
   "source": [
    "Now let's run the agent with the same queries as before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "831be36a-78a1-4885-9a03-8d085dfd7e37",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: pre_model_hook\n",
      "================================\u001b[1m Remove Message \u001b[0m================================\n",
      "\n",
      "\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What's it known for?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "New York City is known for a variety of iconic landmarks, cultural institutions, and vibrant neighborhoods. Some of the most notable features include:\n",
      "\n",
      "1. **Statue of Liberty**: A symbol of freedom and democracy, located on Liberty Island.\n",
      "2. **Times Square**: Known for its bright lights, Broadway theaters, and bustling atmosphere.\n",
      "3. **Central Park**: A large public park offering a natural oasis amidst the urban environment.\n",
      "4. **Empire State Building**: An iconic skyscraper offering panoramic views of the city.\n",
      "5. **Broadway**: Famous for its world-class theater productions and musicals.\n",
      "6. **Wall Street**: The financial hub of the United States, located in the Financial District.\n",
      "7. **Museums**: Including the Metropolitan Museum of Art, Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
      "8. **Diverse Cuisine**: A melting pot of cultures, offering a wide range of international foods.\n",
      "9. **Cultural Diversity**: Known for its diverse population and vibrant cultural scene.\n",
      "10. **Brooklyn Bridge**: An iconic suspension bridge connecting Manhattan and Brooklyn.\n",
      "\n",
      "These are just a few highlights, as NYC is a city with endless attractions and activities.\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "where can i find the best bagel?\n",
      "\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "New York City is famous for its bagels, and there are several places renowned for serving some of the best. Here are a few top spots where you can find delicious bagels in NYC:\n",
      "\n",
      "1. **Ess-a-Bagel**: Known for its large, chewy bagels and a wide variety of spreads and toppings. Locations in Midtown and the East Village.\n",
      "\n",
      "2. **Russ & Daughters**: A historic appetizing store on the Lower East Side, famous for its bagels with lox and cream cheese.\n",
      "\n",
      "3. **Absolute Bagels**: Located on the Upper West Side, this spot is popular for its fresh, fluffy bagels.\n",
      "\n",
      "4. **Murray’s Bagels**: Known for its traditional, hand-rolled bagels. Located in Greenwich Village.\n",
      "\n",
      "5. **Tompkins Square Bagels**: Offers a wide selection of bagels and creative cream cheese flavors. Located in the East Village.\n",
      "\n",
      "6. **Bagel Hole**: A small shop in Park Slope, Brooklyn, known for its classic, no-frills bagels.\n",
      "\n",
      "7. **Leo’s Bagels**: Located in the Financial District, known for its authentic New York-style bagels.\n",
      "\n",
      "Each of these places has its own unique style and flavor, so it might be worth trying a few to find your personal favorite!\n",
      "\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
    "result = graph.invoke(inputs, config=config)\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
    "result = graph.invoke(inputs, config=config)\n",
    "messages = result[\"messages\"]\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
    "print_stream(\n",
    "    graph.stream(inputs, config=config, stream_mode=\"updates\"),\n",
    "    output_messages_key=\"messages\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc9a0604-3d2b-48ff-9eaf-d16ea351fb30",
   "metadata": {},
   "source": [
    "You can see that the `pre_model_hook` node returned the last 3 messages again. However, this time, the message history is modified in the graph state as well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "394f72f8-f817-472d-a193-e01509a86132",
   "metadata": {},
   "outputs": [],
   "source": [
    "updated_messages = graph.get_state(config).values[\"messages\"]\n",
    "assert (\n",
    "    # First 2 messages in the new history are the same as last 2 messages in the old\n",
    "    [(m.type, m.content) for m in updated_messages[:2]]\n",
    "    == [(m.type, m.content) for m in messages[-2:]]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee186d6d-4d07-404f-b236-f662db62339d",
   "metadata": {},
   "source": [
    "## Summarizing message history"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6e53e0f-9a1e-4188-8435-c23ad8148b4f",
   "metadata": {},
   "source": [
    "Finally, let's apply a different strategy for managing message history — summarization. Just as with trimming, you can choose to keep original message history unmodified or overwrite it. The example below will only show the former.\n",
    "\n",
    "We will use the [`SummarizationNode`](https://langchain-ai.github.io/langmem/guides/summarization/#using-summarizationnode) from the prebuilt `langmem` library. Once the message history reaches the token limit, the summarization node will summarize earlier messages to make sure they fit into `max_tokens`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "b9540c1c-2eba-42da-ba4e-478521161a1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# highlight-next-line\n",
    "from langmem.short_term import SummarizationNode\n",
    "from langgraph.prebuilt.chat_agent_executor import AgentState\n",
    "from typing import Any\n",
    "\n",
    "model = ChatOpenAI(model=\"gpt-4o\")\n",
    "summarization_model = model.bind(max_tokens=128)\n",
    "\n",
    "summarization_node = SummarizationNode(\n",
    "    token_counter=count_tokens_approximately,\n",
    "    model=summarization_model,\n",
    "    max_tokens=384,\n",
    "    max_summary_tokens=128,\n",
    "    output_messages_key=\"llm_input_messages\",\n",
    ")\n",
    "\n",
    "\n",
    "class State(AgentState):\n",
    "    # NOTE: we're adding this key to keep track of previous summary information\n",
    "    # to make sure we're not summarizing on every LLM call\n",
    "    # highlight-next-line\n",
    "    context: dict[str, Any]\n",
    "\n",
    "\n",
    "checkpointer = InMemorySaver()\n",
    "graph = create_react_agent(\n",
    "    # limit the output size to ensure consistent behavior\n",
    "    model.bind(max_tokens=256),\n",
    "    tools,\n",
    "    # highlight-next-line\n",
    "    pre_model_hook=summarization_node,\n",
    "    # highlight-next-line\n",
    "    state_schema=State,\n",
    "    checkpointer=checkpointer,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "8eccaaca-5d9c-4faf-b997-d4b8e84b59ac",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Update from node: pre_model_hook\n",
      "================================\u001b[1m System Message \u001b[0m================================\n",
      "\n",
      "Summary of the conversation so far: The user asked about the current weather in New York City. In response, the assistant provided information that it might be cloudy, with a chance of rain, and temperatures reaching up to 80 degrees.\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What's it known for?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "New York City, often referred to as NYC, is known for its:\n",
      "\n",
      "1. **Landmarks and Iconic Sites**:\n",
      "   - **Statue of Liberty**: A symbol of freedom and democracy.\n",
      "   - **Central Park**: A vast green oasis in the middle of the city.\n",
      "   - **Empire State Building**: Once the tallest building in the world, offering stunning views of the city.\n",
      "   - **Times Square**: Known for its bright lights and bustling atmosphere.\n",
      "\n",
      "2. **Cultural Institutions**:\n",
      "   - **Broadway**: Renowned for theatrical performances and musicals.\n",
      "   - **Metropolitan Museum of Art** and **Museum of Modern Art (MoMA)**: World-class art collections.\n",
      "   - **American Museum of Natural History**: Known for its extensive exhibits ranging from dinosaurs to space exploration.\n",
      "   \n",
      "3. **Diverse Neighborhoods and Cuisine**:\n",
      "   - NYC is famous for having a melting pot of cultures, reflected in neighborhoods like Chinatown, Little Italy, and Harlem.\n",
      "   - The city offers a wide range of international cuisines, from street food to high-end dining.\n",
      "\n",
      "4. **Financial District**:\n",
      "   - Home to Wall Street, the New York Stock Exchange (NYSE), and other major financial institutions.\n",
      "\n",
      "5. **Media and Entertainment**:\n",
      "   - Major hub for television, film, and media, with numerous studios and networks based there.\n",
      "\n",
      "6. **Fashion**:\n",
      "   - Often referred to as one of the \"Big Four\" fashion capitals, hosting events like New York Fashion Week.\n",
      "\n",
      "7. **Sports**:\n",
      "   - Known for its passionate sports culture with teams like the Yankees (MLB), Mets (MLB), Knicks (NBA), and Rangers (NHL).\n",
      "\n",
      "These elements, among others, contribute to NYC's reputation as a vibrant and dynamic city.\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "where can i find the best bagel?\n",
      "\n",
      "\n",
      "\n",
      "Update from node: agent\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Finding the best bagel in New York City can be subjective, as there are many beloved spots across the city. However, here are some renowned bagel shops you might want to try:\n",
      "\n",
      "1. **Ess-a-Bagel**: Known for its chewy and flavorful bagels, located in Midtown and Stuyvesant Town.\n",
      "\n",
      "2. **Bagel Hole**: A favorite for traditionalists, offering classic and dense bagels, located in Park Slope, Brooklyn.\n",
      "\n",
      "3. **Russ & Daughters**: A legendary appetizing store on the Lower East Side, famous for their bagels with lox.\n",
      "\n",
      "4. **Murray’s Bagels**: Located in Greenwich Village, known for their fresh and authentic New York bagels.\n",
      "\n",
      "5. **Absolute Bagels**: Located on the Upper West Side, they’re known for their fresh, fluffy bagels with a variety of spreads.\n",
      "\n",
      "6. **Tompkins Square Bagels**: In the East Village, famous for their creative cream cheese options and fresh bagels.\n",
      "\n",
      "7. **Zabar’s**: A landmark on the Upper West Side known for their classic bagels and smoked fish.\n",
      "\n",
      "Each of these spots offers a unique take on the classic New York bagel experience, and trying several might be the best way to discover your personal favorite!\n",
      "\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
    "\n",
    "result = graph.invoke(inputs, config=config)\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
    "result = graph.invoke(inputs, config=config)\n",
    "\n",
    "inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
    "print_stream(graph.stream(inputs, config=config, stream_mode=\"updates\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7caaf2f7-281a-4421-bf98-c745d950c56f",
   "metadata": {},
   "source": [
    "You can see that the earlier messages have now been replaced with the summary of the earlier conversation!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
